﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace DeSleeper.Library.PowerManagement
{
	public class PowerScheme : IEquatable<PowerScheme>
	{
		private const int ERROR_MORE_DATA = 234;
		private const int ERROR_NO_MORE_ITEMS = 259;
		private static readonly Guid GUID_NO_SUBGROUP = new Guid("fea3413e-7e05-4911-9a71-700331f1c294");
		private static readonly Guid GUID_DISK_SUBGROUP = new Guid("0012ee47-9041-4b5d-9b77-535fba8b1442");
		private static readonly Guid GUID_SYSTEM_BUTTON_SUBGROUP = new Guid("4f971e89-eebd-4455-a8de-9e59040e7347");
		private static readonly Guid GUID_PROCESSOR_SETTINGS_SUBGROUP = new Guid("54533251-82be-4824-96c1-47b60b740d00");
		private static readonly Guid GUID_VIDEO_SUBGROUP = new Guid("7516b95f-f776-4464-8c53-06167f40cc99");
		private static readonly Guid GUID_BATTERY_SUBGROUP = new Guid("e73a048d-bf27-4f12-9731-8b2076e8891f");
		private static readonly Guid GUID_SLEEP_SUBGROUP = new Guid("238c9fa8-0aad-41ed-83f4-97be242c8f20");
		private static readonly Guid GUID_PCIEXPRESS_SETTINGS_SUBGROUP = new Guid("501a4d13-42af-4429-9fd1-a8218c268e20");


		private static readonly Guid STANDBYIDLE = new Guid("29f6c1db-86da-48c5-9fdb-f2b67b1f44da");
		private static readonly Guid IDLETHRESHOLD = new Guid("81cd32e0-7833-44f3-8737-7081f38d1f70");
		private static readonly Guid PROCTHROTTLEMAX = new Guid("bc5038f7-23e0-4960-96da-33abaf5935ec");
        private static readonly Guid PROCTHROTTLEMIN = new Guid("893dee8e-2bef-41e0-89c6-b55d0929964c");
		private static readonly Guid VIDEOIDLE = new Guid("3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e");
		private static readonly Guid DISKIDLE = new Guid("6738e2c4-e8a5-4a42-b16a-e040e769756e");

		public uint Index { get; private set; }
		// Not supported before Vista.
		public Guid Guid { get; set; }
		public string Name { get; set; }
		public string Description { get; set; }
		public PowerSettings ACSettings { get; set; }
		public PowerSettings DCSettings { get; set; }

		private PowerScheme(uint index)
		{
			Index = index;
		}

		#region Current Scheme

		/// <exception cref="InvalidOperationException">Current power scheme could not be retrieved.</exception>
		public static PowerScheme GetCurrent()
		{
			var powerPolicy = new POWER_POLICY();
			var globalPowerPolicy = new GLOBAL_POWER_POLICY();
			if (!_getCurrentPowerPolicies(ref globalPowerPolicy, ref powerPolicy))
				throw new InvalidOperationException("Current power scheme could not be retrieved.",
					new Win32Exception(Marshal.GetLastWin32Error()));

			if (!OSHelper.IsVistaOrGreater)
			{
				uint index = 0;
				if (!_getActivePwrScheme(ref index))
					throw new InvalidOperationException("Active power scheme could not be retrieved.",
					                                    new Win32Exception(Marshal.GetLastWin32Error()));

			    return GetPowerSchemes().Single(scheme => scheme.Index == index);
			}

			var guid = VistaPowerHelper.GetActiveScheme();
			return createScheme(guid, 0);
		}

        public static void SetCurrent(PowerScheme scheme)
        {
            if (!OSHelper.IsVistaOrGreater)
            {
                if (!_setActivePwrScheme(scheme.Index, IntPtr.Zero, IntPtr.Zero))
                    throw new InvalidOperationException("Active power scheme could not be set.",
                                                        new Win32Exception(Marshal.GetLastWin32Error()));
            }
            else
            {
                VistaPowerHelper.SetActiveScheme(scheme.Guid);
            }
        }

        [DllImport("powrprof.dll", SetLastError = true, EntryPoint = "GetCurrentPowerPolicies")]
		private static extern bool _getCurrentPowerPolicies(ref GLOBAL_POWER_POLICY globalPowerPolicy, ref POWER_POLICY powerPolicy);

        [DllImport("powrprof.dll", SetLastError = true, EntryPoint = "GetActivePwrScheme")]
		private static extern bool _getActivePwrScheme(ref uint index);


		#endregion

		#region Enumeration
		/// <exception cref="InvalidOperationException">System Power Schemes could not be enumerated.</exception>
		public static IEnumerable<PowerScheme> GetPowerSchemes()
		{
			if (!OSHelper.IsVistaOrGreater)
			{
				var collection = new PowerSchemeCollection();
				if (!_enumPwrSchemes(collection.ReceivePowerScheme, 0))
					throw wrapLastWin32Error("System Power Schemes could not be enumerated.");

				return collection;
			}

			var schemeGuids = VistaPowerHelper.GetPowerEnumeration<Guid>(Guid.Empty, Guid.Empty, POWER_DATA_ACCESSOR.ACCESS_SCHEME);
			return schemeGuids.Select<Guid, PowerScheme>(createScheme);
		}

		private static InvalidOperationException wrapLastWin32Error(string message)
		{
			return new InvalidOperationException(message, new Win32Exception(Marshal.GetLastWin32Error()));
		}

        [DllImport("powrprof.dll", SetLastError = true, EntryPoint = "EnumPwrSchemes")]
		private static extern bool _enumPwrSchemes(PwrSchemesEnumProc pwrSchemesEnumProc, int param);

		/// <summary>
		/// Delegate to receive enumerated power policies (XP and lower)
		/// </summary>
		/// <param name="index">Power scheme index.</param>
		/// <param name="nameLength">Size of the <paramref name="name"/> string, in bytes.</param>
		/// <param name="name">Name of the power scheme.</param>
		/// <param name="descLength">Size of the description string, in bytes.</param>
		/// <param name="description">Description of the power scheme.</param>
		/// <param name="powerPolicy">Receives the power policy.</param>
		/// <param name="param">User-defined value</param>
		/// <returns></returns>
		private delegate bool PwrSchemesEnumProc(
			uint index, 
			UInt32 nameLength, 
			[MarshalAs(UnmanagedType.LPWStr)] string name, 
			UInt32 descLength,
			[MarshalAs(UnmanagedType.LPWStr)] string description,
			ref POWER_POLICY powerPolicy,  
			int param
			);

		private class PowerSchemeCollection : List<PowerScheme>
		{
			public bool ReceivePowerScheme(uint index,
                UInt32 nameLength, string name,
				UInt32 descLength, string description,
				ref POWER_POLICY powerPolicy, int param)
			{	
				var scheme = createScheme(powerPolicy, index, name, description);
				Add(scheme);
				return true;
			}
		}
		#endregion

		#region Update

		/// <exception cref="InvalidOperationException">Failed to retrieve power policy for update, index: {0}.</exception>
		/// <exception cref="InvalidOperationException">Failed to write power policy for update, index: {0}.</exception>
		public void Update()
		{
			if (!OSHelper.IsVistaOrGreater)
				updateXP();
			else
				updateVista();
		}

	    ///<summary>
	    /// Copy this scheme to a new scheme with identical settings.
	    ///</summary>
	    ///<param name="newSchemeName">The name to give the new scheme.</param>
	    ///<param name="description">The description to apply to the new scheme.  Ignored by Windows XP.</param>
	    ///<returns>A new scheme.</returns>
	    public PowerScheme Duplicate(string newSchemeName, string description)
        {
            if (!OSHelper.IsVistaOrGreater)
            {
                var newIndex = GetPowerSchemes().Max(scheme => scheme.Index) + 1;
                writeXP(newIndex, newSchemeName);
                return GetPowerSchemes().Single(scheme => scheme.Index == newIndex);
            }
            else
            {
                var newGuid = VistaPowerHelper.DuplicateScheme(Guid);
                VistaPowerHelper.WritePowerValue(newGuid, Guid.Empty, Guid.Empty, PowerValueType.FriendlyName, newSchemeName);
                VistaPowerHelper.WritePowerValue(newGuid, Guid.Empty, Guid.Empty, PowerValueType.Description, description);
                return GetPowerSchemes().Single(scheme => scheme.Guid == newGuid);
            }
        }

		private void updateVista()
		{	
			writeSettings(PowerValueType.ACIndex, ACSettings);
			writeSettings(PowerValueType.DCIndex, DCSettings);
		}

		private void writeSettings(PowerValueType settingsType, PowerSettings settings)
		{
			VistaPowerHelper.WritePowerValue(Guid, GUID_SLEEP_SUBGROUP, STANDBYIDLE, settingsType, timespanToSeconds(settings.IdleTimeout));
			VistaPowerHelper.WritePowerValue(Guid, GUID_SLEEP_SUBGROUP, IDLETHRESHOLD, settingsType, (uint)percentageToByte(settings.IdleThreshold));
            if (settings.ProcessorPowerSettings != ProcessorPowerSettings.None)
            {
                var processorThrottleMinimum =
                    byteToPercentage((byte)VistaPowerHelper.ReadPowerValue<uint>(Guid, GUID_PROCESSOR_SETTINGS_SUBGROUP,
                                                                           PROCTHROTTLEMIN, settingsType));
                // If no throttling is in place set to 5% minimum.  Else leave unchanged.
                if (processorThrottleMinimum == 1m)
                {
                    VistaPowerHelper.WritePowerValue(Guid, GUID_PROCESSOR_SETTINGS_SUBGROUP, PROCTHROTTLEMIN,
                                                     settingsType,
                                                     (uint)percentageToByte(0.05m));
                }
            }
		    VistaPowerHelper.WritePowerValue(Guid, GUID_PROCESSOR_SETTINGS_SUBGROUP, PROCTHROTTLEMAX, settingsType, (uint)percentageToByte(settings.ForcedThrottle));
			VistaPowerHelper.WritePowerValue(Guid, GUID_DISK_SUBGROUP, DISKIDLE, settingsType, timespanToSeconds(settings.HardDriveTimeout));
			VistaPowerHelper.WritePowerValue(Guid, GUID_VIDEO_SUBGROUP, VIDEOIDLE, settingsType, timespanToSeconds(settings.VideoTimeout));
		}

		private void updateXP()
		{
			writeXP(Index, Name);
		}

        /// <exception cref="InvalidOperationException">Failed to retrieve power policy for update, index: {0}.</exception>
        /// <exception cref="InvalidOperationException">Failed to write power policy for update, index: {0}.</exception>
        private void writeXP(uint newIndex, string newSchemeName)
        {
            var powerPolicy = new POWER_POLICY();
            if (!_readPwrScheme(Index, ref powerPolicy))
                throw new InvalidOperationException(
                    string.Format(CultureInfo.CurrentUICulture,
                                  "Failed to retrieve power policy for update, index: {0}.",
                                  Index),
                    new Win32Exception(Marshal.GetLastWin32Error()));

            updatePolicyFromCurrentSettings(ref powerPolicy);

            try
            {
                var description = (Index == newIndex) ? Description : newSchemeName;
                if (!_writePwrScheme(ref newIndex, newSchemeName, description, ref powerPolicy))
                    throw new Win32Exception(Marshal.GetLastWin32Error());
            }
            catch (Exception e)
            {
                throw new InvalidOperationException(
                        string.Format(CultureInfo.CurrentUICulture,
                                      "Failed to write power policy for update, source/destination index: {0}/{1}, source/destination name: {2}/{3}.",
                                      Index, newIndex, Name, newSchemeName), e);
            }
        }

        [DllImport("powrprof.dll", SetLastError = true, EntryPoint = "ReadPwrScheme")]
		private static extern bool _readPwrScheme(uint index,
			ref POWER_POLICY powerPolicy);

        [DllImport("powrprof.dll", SetLastError = true, EntryPoint = "WritePwrScheme")]
		private static extern bool _writePwrScheme(ref uint index,
			[MarshalAs(UnmanagedType.LPWStr)] string name,
			[MarshalAs(UnmanagedType.LPWStr)] string description,
			ref POWER_POLICY powerPolicy);

        [DllImport("powrprof.dll", SetLastError = true, EntryPoint = "SetActivePwrScheme")]
		private static extern bool _setActivePwrScheme(uint index,
			IntPtr globalPowerPolicy,
			IntPtr powerPolicy);
		#endregion

		#region Conversion methods
		private void updatePolicyFromCurrentSettings(ref POWER_POLICY policy)
		{
			policy.UserPolicy.IdleTimeoutAc = timespanToSeconds(ACSettings.IdleTimeout);
			policy.UserPolicy.IdleSensitivityAc = percentageToByte(ACSettings.IdleThreshold);
		    policy.UserPolicy.ThrottlePolicyAc = processorPowerSettingsToThrottlePolicy(ACSettings.ProcessorPowerSettings);
			policy.UserPolicy.SpindownTimeoutAc = timespanToSeconds(ACSettings.HardDriveTimeout);
			policy.UserPolicy.VideoTimeoutAc = timespanToSeconds(ACSettings.VideoTimeout);
			policy.UserPolicy.ForcedThrottleAc = percentageToByte(ACSettings.ForcedThrottle);
			policy.UserPolicy.FanThrottleToleranceAc = percentageToByte(ACSettings.ThrottleBeforeFan);
			policy.UserPolicy.OptimizeForPowerAc = ACSettings.FavorFanOverThrottle;
			policy.UserPolicy.MaxSleepAc = ACSettings.SleepTargetState;

			policy.UserPolicy.IdleTimeoutDc = timespanToSeconds(DCSettings.IdleTimeout);
			policy.UserPolicy.IdleSensitivityDc = percentageToByte(DCSettings.IdleThreshold);
            policy.UserPolicy.ThrottlePolicyDc = processorPowerSettingsToThrottlePolicy(DCSettings.ProcessorPowerSettings);
			policy.UserPolicy.SpindownTimeoutDc = timespanToSeconds(DCSettings.HardDriveTimeout);
			policy.UserPolicy.VideoTimeoutDc = timespanToSeconds(DCSettings.VideoTimeout);
			policy.UserPolicy.ForcedThrottleDc = percentageToByte(DCSettings.ForcedThrottle);
			policy.UserPolicy.FanThrottleToleranceDc = percentageToByte(DCSettings.ThrottleBeforeFan);
			policy.UserPolicy.OptimizeForPowerDc = DCSettings.FavorFanOverThrottle;
			policy.UserPolicy.MaxSleepDc = DCSettings.SleepTargetState;
		}

	    private static PowerScheme createScheme(POWER_POLICY powerPolicy, uint index, string name, string description)
		{
			var acSettings =
				new PowerSettings
				{
					IdleTimeout = secondsToTimespan(powerPolicy.UserPolicy.IdleTimeoutAc),
					IdleThreshold = byteToPercentage(powerPolicy.UserPolicy.IdleSensitivityAc),
                    ProcessorPowerSettings = throttlePolicyToPowerSettings(powerPolicy.UserPolicy.ThrottlePolicyAc), 
					HardDriveTimeout = secondsToTimespan(powerPolicy.UserPolicy.SpindownTimeoutAc),
					VideoTimeout = secondsToTimespan(powerPolicy.UserPolicy.VideoTimeoutAc),
					FavorFanOverThrottle = powerPolicy.UserPolicy.OptimizeForPowerAc,
					ForcedThrottle = byteToPercentage(powerPolicy.UserPolicy.ForcedThrottleAc),
					ThrottleBeforeFan = byteToPercentage(powerPolicy.UserPolicy.FanThrottleToleranceAc),
					SleepTargetState = powerPolicy.UserPolicy.MaxSleepAc
				};

			var dcSettings =
				new PowerSettings
				{
					IdleTimeout = secondsToTimespan(powerPolicy.UserPolicy.IdleTimeoutDc),
					IdleThreshold = byteToPercentage(powerPolicy.UserPolicy.IdleSensitivityDc),
                    ProcessorPowerSettings = throttlePolicyToPowerSettings(powerPolicy.UserPolicy.ThrottlePolicyDc), 
					HardDriveTimeout = secondsToTimespan(powerPolicy.UserPolicy.SpindownTimeoutDc),
					VideoTimeout = secondsToTimespan(powerPolicy.UserPolicy.VideoTimeoutDc),
					FavorFanOverThrottle = powerPolicy.UserPolicy.OptimizeForPowerDc,
					ForcedThrottle = byteToPercentage(powerPolicy.UserPolicy.ForcedThrottleDc),
					ThrottleBeforeFan = byteToPercentage(powerPolicy.UserPolicy.FanThrottleToleranceDc),
					SleepTargetState = powerPolicy.UserPolicy.MaxSleepDc
				};

			return new PowerScheme(index)
			{
				Name = name,
				Description = description,
				ACSettings = acSettings,
				DCSettings = dcSettings
			};
		}

	    private static ProcessorPowerSettings throttlePolicyToPowerSettings(byte throttlePolicy)
	    {
	        return (ProcessorPowerSettings) throttlePolicy;
	    }

        private static byte processorPowerSettingsToThrottlePolicy(ProcessorPowerSettings settings)
        {
            return (byte)settings;
        }

	    private static PowerScheme createScheme(Guid schemeGuid, int index)
		{
			return
				new PowerScheme((uint)index)
				{
					Guid = schemeGuid,
					ACSettings = createSettings(schemeGuid, PowerValueType.AC),
					DCSettings = createSettings(schemeGuid, PowerValueType.DC),
					Description = VistaPowerHelper.ReadPowerValue<string>(schemeGuid, Guid.Empty, Guid.Empty, PowerValueType.Description),
					Name = VistaPowerHelper.ReadPowerValue<string>(schemeGuid, Guid.Empty, Guid.Empty, PowerValueType.FriendlyName)
				};
		}

		private static PowerSettings createSettings(Guid schemeGuid, PowerValueType settingsType)
		{
            return new PowerSettings
                    {
                        IdleTimeout = secondsToTimespan(VistaPowerHelper.ReadPowerValue<uint>(schemeGuid, GUID_SLEEP_SUBGROUP, STANDBYIDLE, settingsType)),
                        IdleThreshold = byteToPercentage((byte)VistaPowerHelper.ReadPowerValue<uint>(schemeGuid, GUID_SLEEP_SUBGROUP, IDLETHRESHOLD, settingsType)),
                        ForcedThrottle = byteToPercentage((byte)VistaPowerHelper.ReadPowerValue<uint>(schemeGuid, GUID_PROCESSOR_SETTINGS_SUBGROUP, PROCTHROTTLEMAX, settingsType)),
                        HardDriveTimeout = secondsToTimespan(VistaPowerHelper.ReadPowerValue<uint>(schemeGuid, GUID_DISK_SUBGROUP, DISKIDLE, settingsType)),
                        VideoTimeout = secondsToTimespan(VistaPowerHelper.ReadPowerValue<uint>(schemeGuid, GUID_VIDEO_SUBGROUP, VIDEOIDLE, settingsType)),
                        // If throttling is 100% then it's none, else adaptive.
                        ProcessorPowerSettings = byteToPercentage((byte)VistaPowerHelper.ReadPowerValue<uint>(schemeGuid, GUID_PROCESSOR_SETTINGS_SUBGROUP, PROCTHROTTLEMIN, settingsType)) == 1
                            ? ProcessorPowerSettings.None : ProcessorPowerSettings.Adaptive
                    };
		}

		private static TimeSpan secondsToTimespan(uint seconds)
		{
			return new TimeSpan(0, 0, 0, (int)seconds);
		}

		private static decimal byteToPercentage(byte sensitivity)
		{
			return sensitivity / 100m;
		}

		private static uint timespanToSeconds(TimeSpan seconds)
		{
			return (uint)seconds.TotalSeconds;
		}

		private static byte percentageToByte(decimal percentage)
		{
			return (byte)(percentage * 100);
		}
		#endregion

		#region PInvoke Structures
		private struct POWER_POLICY
		{
			public USER_POWER_POLICY UserPolicy;
			public MACHINE_POWER_POLICY MachinePolicy;
		}

		[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
		private struct USER_POWER_POLICY
		{
			public uint Revision;
			public POWER_ACTION_POLICY IdleAc;
			public POWER_ACTION_POLICY IdleDc;
			public uint IdleTimeoutAc;
			public uint IdleTimeoutDc;
			public byte IdleSensitivityAc;
			public byte IdleSensitivityDc;
			public byte ThrottlePolicyAc;
			public byte ThrottlePolicyDc;
			public SystemPowerState MaxSleepAc;
			public SystemPowerState MaxSleepDc;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
			public uint[] Reserved;
			public uint VideoTimeoutAc;
			public uint VideoTimeoutDc;
			public uint SpindownTimeoutAc;
			public uint SpindownTimeoutDc;
			[MarshalAs(UnmanagedType.I1)]
			public bool OptimizeForPowerAc;
			[MarshalAs(UnmanagedType.I1)]
			public bool OptimizeForPowerDc;
			public byte FanThrottleToleranceAc;
			public byte FanThrottleToleranceDc;
			public byte ForcedThrottleAc;
			public byte ForcedThrottleDc;
		}

		private struct MACHINE_POWER_POLICY
		{
			public byte Revision;
			public SystemPowerState MinSleepAc;
			public SystemPowerState MinSleepDc;
			public SystemPowerState ReducedLatencySleepAc;
			public SystemPowerState ReducedLatencySleepDc;
			public uint DozeTimeoutAc;
			public uint DozeTimeoutDc;
			public uint DozeS4TimeoutAc;
			public uint DozeS4TimeoutDc;
			public byte MinThrottleAc;
			public byte MinThrottleDc;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
			public byte[] pad1;
			public POWER_ACTION_POLICY OverThrottledAc;
			public POWER_ACTION_POLICY OverThrottledDc;
		}

		[StructLayout(LayoutKind.Sequential, Pack = 1)]
		struct GLOBAL_POWER_POLICY
		{
			public GLOBAL_USER_POWER_POLICY UserPolicy;
			public GLOBAL_MACHINE_POWER_POLICY MachinePolicy;
		}

		[StructLayout(LayoutKind.Sequential, Pack = 1)]
		private struct GLOBAL_MACHINE_POWER_POLICY
		{
			public uint Revision;
			public SystemPowerState LidOpenWakeAc;
			public SystemPowerState LidOpenWakeDc;
			public uint BroadcastCapacityResolution;
		}

		[StructLayout(LayoutKind.Sequential, Pack = 1)]
		private struct GLOBAL_USER_POWER_POLICY
		{
			public const int NUM_DISCHARGE_POLICIES = 4;

			public uint Revision;
			public POWER_ACTION_POLICY PowerButtonAc;
			public POWER_ACTION_POLICY PowerButtonDc;
			public POWER_ACTION_POLICY SleepButtonAc;
			public POWER_ACTION_POLICY SleepButtonDc;
			public POWER_ACTION_POLICY LidCloseAc;
			public POWER_ACTION_POLICY LidCloseDc;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = NUM_DISCHARGE_POLICIES)]
			public SYSTEM_POWER_LEVEL[] DischargePolicy;
			public GlobalPowerPolicyFlags GlobalFlags;
		}

		[StructLayout(LayoutKind.Sequential)]
		private struct SYSTEM_POWER_LEVEL
		{
			public byte Enable;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.I1)]
			public byte[] Spare;
			public uint BatteryLevel;
			public POWER_ACTION_POLICY PowerPolicy;
			public SystemPowerState MinSystemState;
		}

		[StructLayout(LayoutKind.Sequential, Pack = 1)]
		private struct POWER_ACTION_POLICY
		{
			public PowerAction Action;
			public PowerActionFlags Flags;
			public PowerActionEventLevels EventCode;
		}
		#endregion

	    #region Implementation of IEquatable<PowerScheme>

	    public bool Equals(PowerScheme other)
	    {
            if (!OSHelper.IsVistaOrGreater)
                return other.Index == Index;

	        return other.Guid == Guid;
	    }

	    #endregion
	}
}